PARSER_CREATOR = {
    ready: false, // Are we ready to let the user do their part?
    frame: null,  // Window object of the website view iframe.
    doc: null,    // Document object of the website view iframe.
    currentParserId: null,
    currentCrawlerId: null,
    currentFieldsetId: null,
    currentSamplePageId: null,
    temporaryParserId: null,
    // When editing an existing parser we need to create a new temporary parser without deleting the old one. This will hold the information about whether we already did that.
    temporaryParserSwitched: false, 
    pointAndClick: {
        active: false, 
        currentTarget: null
    },

    preInitVariables: function(){
        this.currentParserId = $('#parser-creator-fields input[name=id]').val();
        this.currentCrawlerId = $('#parser-creator-fields input[name=crawler_id]').val();
        this.currentFieldsetId = $('#parser-creator-fields input[name=fieldset_id]').val();
        this.temporaryParserId = parseInt($('#parser-creator-fields input[name=temporary_id]').val(), 10) || null;
    },

    preInitFieldsManager: function(){
        this.preInitFieldSelector();
        this.preInitTemporaryParserSaving();
    },

    preInitFieldSelector: function(){
        var that = PARSER_CREATOR;
        var $fields = $('#parser-creator-fields');
        $('li', $fields).click(function(){
            if (!$(this).hasClass('active')) {
                $('li.active', $fields).removeClass('active');
                $(this).addClass('active');
                that.addFieldDetailsWrapper(this);
                $('li .details', $fields).hide();
                $('.details', this).show();
                that.removeFramePointAndClick();
                that.addFramePointAndClick($('select[name^=selector_] option:selected', this).val());
            }
        });

        $('#parser-creator-fields').on('change', 'select[name=filters]', that.addFieldFilter);
        that.addFilterTestHandler();
        that.addFilterRemoveHandler();

        if (!$('li.active', $fields).length) {
            $('li:first', $fields).trigger('click');
        }
    },

    preInitTemporaryParserSaving: function(){
        var $form = $('#parser-creator-fields form');
        var formData = $form.serialize();
        var that = this;

        $form.on('change', function(e){
            var newFormData = $form.serialize();

            if (formData !== newFormData) {
                newFormData = newFormData.replace(/(^|\?|&)id=\d+/, ''); // Remove the original parser's id if present. We don't want to save that, we want a temporary parser.
                newFormData += '&temporary=1';

                if (that.currentParserId && !that.temporaryParserSwitched) {
                    that.temporaryParserId = null;
                    that.temporaryParserSwitched = true;
                }

                if (that.temporaryParserId) {
                    newFormData += '&id=' + that.temporaryParserId;
                }

                $.ajax({
                    type: 'post',
                    url: that.temporaryParserId ? '/parser/edit?id=' + that.temporaryParserId : '/parser/add?crawler_id=' + that.currentCrawlerId + '&fieldset_id=' + that.currentFieldsetId,
                    data: newFormData,
                    success: function(id){
                        that.temporaryParserId = id;
                        that.getSampleData(true);
                    },
                    error: function(res){ genericError(res) }
                });
                formData = newFormData;
            }
        });

        $form.on('submit', function(){
            if (that.temporaryParserId) {
                $form.get(0).temporary_id.value = that.temporaryParserId;
            }
        })
    },

    addFilterTestHandler: function(){
        var testFiltersAjax;
        $('#parser-creator-fields li').on('change, keyup', '.details input', function(){
            var $details = $(this).closest('.details');
            var posField = $details.closest('li').get(0).id.match(/field_(\d+)/)[1];
            var data = $(this).closest('form').serializeArray();
            for (var i in data) {
                var posData = data[i].name.match(/_(\d+)/);
                if (data[i].name.search('filters_') === -1 || !posData || posData[1] !== posField) {
                    delete data[i];
                }
            }
            data.push({ name: 'exampleContent', value: trim($('p.example', $details).attr('data-orig')) });
            testFiltersAjax && testFiltersAjax.abort();
            testFiltersAjax = $.ajax({
                url: '/parser/test-filters',
                type: 'get',
                data: data,
                success: function(res){
                    $('.example', $details).text(res);
                }
            });
        });
    },

    addFilterRemoveHandler: function(){
        $('#parser-creator-fields li').on('click', '.filters > div > i', function(){
            var $details = $(this).closest('.details');
            $(this).parent().remove();
            if(!$('.filters > div', $details).length) {
                $('p.example', $details).text($('p.example', $details).attr('data-orig'));
            }
            else {
                $('.filters input:eq(0)', $details).trigger('keyup');
            }
        });
    },

    initFieldsManager: function(){
        var that = this;
        $(this.frame).on('webview-element-select', function(e, el, selectors, nonUser){
            if (!nonUser) {
                that.setFieldFilters();
            }
            that.setFieldSelectors(selectors);
            that.setFieldExample(el);
        });
        if ($('#parser-creator-fields li.active').length) {
            PARSER_CREATOR.addFramePointAndClick($('#parser-creator-fields li.active select[name^=selector_] option:selected').val());
        }
    },

    addFieldDetailsWrapper: function(li){
        if (!$('.details', li).length) {
            $('<div class="details"></div>').appendTo(li);
        }
    },

    setFieldExample: function(el){
        var $details = $('#parser-creator-fields li.active .details');
        $('.example-wrapper', $details).remove();
        $details.append('<div class="example-wrapper"><p><small>Example content:</small></p><p class="example"></p></div>');
        var $example = $('.example', $details);
        $example.attr('data-orig', $(el).outerHtml());
        $details.find('.filters input').eq(0).trigger('keyup');
    },

    setFieldFilters: function(){
        var that = PARSER_CREATOR;
        var $details = $('#parser-creator-fields li.active .details');
        $('.filters-wrapper', $details).remove();
        $details.append('<div class="filters-wrapper"><p><small>Filters:</small></p></div>');

        $filters = $(PARSER_FILTERS.getSelectElement());
        $details.find('.filters-wrapper').append($filters);
        $details.find('.filters-wrapper').append('<div class="filters"></div>');

        var $field = $details.closest('li[id^=field]');
        var fieldId = $field.attr('id').match(/\d+$/)[0];
        var dataType = $field.attr('data-type');

        $('.filters', $details).prepend('<input type="hidden" name="filters_' + fieldId + '[DataType][type]" value="' + dataType + '">');
    },

    setFieldSelectors: function(selectors){
        var $details = $('#parser-creator-fields li.active .details');
        $('hr, .selector-wrapper', $details).remove();
        var posField = $details.closest('li').get(0).id.match(/field_(\d+)/)[1];

        $details.prepend('<hr><div class="selector-wrapper"><p><small>Selector:</small></p></div>');
        var $selector = $('<select name="selector_' + posField + '"></select>');
        for (var i in selectors) {
            $selector.append('<option>' + selectors[i] + '</option>');
            $('<input type="hidden" name="selectors_' + posField + '[]">').val(selectors[i]).appendTo($details.find('.selector-wrapper'));
        }
        $details.find('> .selector-wrapper').append($selector);
        $selector.trigger('change');
    },

    addFieldFilter: function(){
        var activeFilter = PARSER_FILTERS[$('option:selected', this).val()];
        if (activeFilter) {
            var posField = $(this).closest('li').get(0).id.match(/field_(\d+)/)[1];
            var paramsFormPart = '<div>' + activeFilter.paramsForm.replace(/filters_%/g, 'filters_' + posField) + '</div>';
            $(this).next('.filters').append(paramsFormPart).find('input:first').trigger('keyup');
        }
    },

    initFrame: function(){
        this.frame = frames['parser-website-view'];

        var that = this;

        this.getFrameDocument(this.frame, function(doc){
            that.doc = doc;
            that.currentSamplePageId = $('input[name=crawler-page-id]', doc).val() || null;

            $(doc).ready(function(){ 
                var cssHref = String(location).match(/(^.*?\/\/.*?)\//)[1] + '/css/iframe-website-view.css';
                var link = '<link rel="stylesheet" href="' + cssHref + '">';
                $('head', that.doc).append(link);
                that.ready = true;
                // Remove the locking overlay. In most cases the user won't even see it.
                $('#parser-website-view .lock').remove();
                $(window).trigger('webview-init');
                $(doc).on('click', function(){ return false });
            });
        })
    },

    preInitWindowResize: function(){
        var that = this;
        $(window).resize(function(){
            that.adjustFrameSize();
        });
        that.adjustFrameSize();
    },

    /**
     * @param  {string} initialSelector If an element in the iframe has to be selected, send its selector here.
     * @return {void}                 
     */
    addFramePointAndClick: function(initialSelector){
        var that = this;
        if (that.doc) {
            var markerHtml = '<div id="crawler-point-and-click"><i class="t"></i><i class="r"></i><i class="b"></i><i class="l"></i><b></b></div>';
            $(markerHtml).appendTo($('body', that.doc));
            var $doc = $(that.doc);

            $doc.on('mousemove', that.handlePointAndClickMouseMove);
            that.pointAndClick.active = true;
            $doc.unbind('click').on('click', that.handlePointAndClickMouseClick);

            if (initialSelector) {
                $(initialSelector, $doc).trigger('mousemove').trigger('click', [true]);
            }
        }
    },

    removeFramePointAndClick: function(){
        var that = PARSER_CREATOR;
        if (that.doc) {
            $('#crawler-point-and-click', that.doc).remove();
            $(that.doc).unbind('mousemove, click').on('click', function(){ return false });
            that.pointAndClick.active = false;
            that.pointAndClick.currentTarget = false;
        }
    },

    handlePointAndClickMouseMove: function(e){
        var that = PARSER_CREATOR;
        var $marker = $('#crawler-point-and-click', that.doc);
        var currentTarget = that.pointAndClick.currentTarget;

        if (e.target !== currentTarget){
            that.pointAndClick.currentTarget = currentTarget = e.target;
            var currentSelectors = that.pointAndClick.currentSelectors = that.getElementSelectors(currentTarget);

            var $target = $(currentTarget);
            var $targetOffset = $target.offset();
            var $markerOffset = $marker.offset();
            var animTime = 250;

            var distanceOrWidthDiffTooBig = Math.abs($markerOffset.top - $targetOffset.top) > 500 || 
                Math.abs($markerOffset.left - $targetOffset.left) > 500 || 
                Math.abs($('.t', $marker).width() - $target.width()) > 500 || 
                Math.abs($('.l', $marker).height() - $target.height()) > 500;

            if (distanceOrWidthDiffTooBig) {
                animTime = 0; // If the difference is too big, the animation won't look good. Let's do it instantaneously then.
            }

            $marker.stop().animate({ top: $targetOffset.top, left: $targetOffset.left}, animTime);
            $('i, b', $marker).stop();
            $('.t', $marker).animate({ width:  $target.outerWidth()  + 1 + 'px' }, animTime);
            $('.l', $marker).animate({ height: $target.outerHeight() + 1 + 'px' }, animTime);
            $('.b', $marker).animate({ top: $target.outerHeight() + 'px', width:  $target.outerWidth()  + 1 + 'px' }, animTime);
            $('.r', $marker).animate({ left: $target.outerWidth() + 'px', height: $target.outerHeight() + 1 + 'px' }, animTime);
            $('b', $marker).text(currentSelectors[0]).animate({ top: $target.outerHeight() + 'px' }, animTime);
        }
    },

    /**
     * @param  {Event} e       
     * @param  {Boolean} nonUser If the source of event is not the user clicking, it's set to true.
     */
    handlePointAndClickMouseClick: function(e, nonUser){
        if (e.which === 1 || nonUser) {
            var that = PARSER_CREATOR;        
            var $marker = $('#crawler-point-and-click', that.doc);
            var $doc = $(that.doc);
            that.pointAndClick.active = !that.pointAndClick.active;

            if (!that.pointAndClick.active) {
                $doc.unbind('mousemove');
                $marker.addClass('selected');
                $(that.frame).trigger('webview-element-select', [that.pointAndClick.currentTarget, that.pointAndClick.currentSelectors, nonUser]);
            }
            else{
                $doc.on('mousemove', that.handlePointAndClickMouseMove);
                $marker.removeClass('selected');
                that.handlePointAndClickMouseMove(e);
            }
            return false;
        }
    },

    /**
     * Returns element selectors in order from probably best to probably worst. 
     * 
     * Best = as few selectors as possible but still able to identify the desired data.
     * 
     * @param  {DOMElement} el
     * @return {Array} At least one selector.
     */
    getElementSelectors: function(el){
        var that = PARSER_CREATOR;

        var xPath = that.getElementPseudoXPath(el);
        var cssPath = that.getElementCssPath(el);
        var cssPathMin = that.getElementCssPathMin(el, cssPath);
        var cssXPath = that.getElementCssPseudoXPath(el);

        var proposedSelectors = [cssPathMin, cssPath, cssXPath, xPath]; // Preferred first.
        var acceptedSelectors = [];

        for (var i in proposedSelectors) {
            var isUnique = $(proposedSelectors[i], that.doc).length === 1;             // Does the selector identify a unique element?
            var isCorrect = $(el).text() === $(proposedSelectors[i], that.doc).text(); // Does the selector identify what we want?

            if (isUnique && isCorrect){ 
                acceptedSelectors.push(proposedSelectors[i]);
            }
        }
        return acceptedSelectors;
    },

    getElementCssPseudoXPath: function(element){
        var paths = [];
        var cssStarted = false;

        for (; element && element.nodeType == 1; element = element.parentNode)  {
            var index = 0;
            var tagName = element.nodeName.toLowerCase();
            var className = (element && element.id && '#' + element.id || '') + (trim(element.className) && '.' + trim(element.className).replace(/( .*)?$/, '') || '');

            if (className){
                cssStarted = true;
                paths.splice(0, 0, className); 
            }
            else if (!cssStarted) {
                // EXTRA TEST FOR ELEMENT.ID
                if (element && element.id) {
                    paths.splice(0, 0, '#' + element.id);
                    continue;
                }

                for (var sibling = element.previousSibling; sibling; sibling = sibling.previousSibling) {
                    // Ignore document type declaration.
                    if (sibling.nodeType == Node.DOCUMENT_TYPE_NODE)
                        continue;

                    if (sibling.nodeName == element.nodeName)
                        ++index;
                }

                var tagName = element.nodeName.toLowerCase();
                var pathIndex = ":eq(" + index + ")";
                paths.splice(0, 0, tagName + pathIndex);
            }
        }

        return paths.length ? paths.join(' ') : null;
    },

    getElementCssPath: function(element){
        var startElement = element;
        var paths = [];

        for (; element && element.nodeType == 1; element = element.parentNode)  {
            var index = 0;
            var tagName = element.nodeName.toLowerCase();
            var className = (element && element.id && '#' + element.id || '') + (trim(element.className) && '.' + trim(element.className).replace(/( .*)?$/, '') || '');
            // We used to do it like this. The difference is we're taking all the classes from the given element, e.g.
            // ".promo.deal.new" instead of just ".promo". The problem with that is that if there are a lot of classes
            // on a single element, it's likely some of them are extra details not present on all pages. Let's assume only 
            // the first one is important.
            // var className = (element && element.id && '#' + element.id || '') + (element.className && element.className.replace(/^| +/g, '.').replace(/\.$/, '') || '');
            if (className) {
                paths.splice(0, 0, className);                
            }
        }

        var path = paths.length ? paths.join(" ") : null

        return path;
    },

    /**
     * Get a possible minimal css path.
     * @param  {string} path 
     * @return {string}      
     */
    getElementCssPathMin: function(element, path){
        if (path) {
            var lastPathPartMatch = path.match(/ (\.[^ ]*)$/);
            var lastPathPart = lastPathPartMatch && lastPathPartMatch[1] || '';
            path = path.replace(/\.[^ ]*?( |$)/g, ' ');
            path = path + ' ' + lastPathPart;
            return path;
        }
        else {
            return null;
        }
    },

    /**
     * An XPath-like jQuery path. Only ids, element names and indexes.
     * @param  {DOMElement} element 
     * @return {string}         
     */
    getElementPseudoXPath: function(element) {
        var paths = [];

        // Use nodeName (instead of localName) so namespace prefix is included (if any).
        for (; element && element.nodeType == 1; element = element.parentNode)  {
            var index = 0;
            // EXTRA TEST FOR ELEMENT.ID
            if (element && element.id) {
                paths.splice(0, 0, '#' + element.id);
                continue;
            }

            for (var sibling = element.previousSibling; sibling; sibling = sibling.previousSibling) {
                // Ignore document type declaration.
                if (sibling.nodeType == Node.DOCUMENT_TYPE_NODE)
                    continue;

                if (sibling.nodeName == element.nodeName)
                    ++index;
            }

            var tagName = element.nodeName.toLowerCase();
            var pathIndex = ":eq(" + index + ")";
            paths.splice(0, 0, tagName + pathIndex);
        }

        return paths.length ? paths.join(' > ') : null;
    },

    /**
     * Adjust the frame size so that we can make using the website view as comfortable as possible, i.e. preferably 
     * no horizontal scrollbar and as much height as possible within the window boundaries.
     * @return {[type]} [description]
     */
    adjustFrameSize: function(){
        var winWidth = $(window).width(), winHeight = $(window).height();

        var parserCreatorWidth = $('#parser-creator-fields').outerWidth() + $('#parser-website-view').outerWidth();
        var frameHeight = $('#parser-website-view').height();

        var widthDiff = winWidth - parserCreatorWidth;
        var heightDiff = winHeight - frameHeight;


        $('#parser-website-view, #parser-data-preview-wrapper').each(function(){
            var $this = $(this);
            $this.width($this.width() + widthDiff - 50);
        })

        $('#parser-website-view, #parser-website-view iframe, #parser-website-view .lock').each(function(){
            var $this = $(this);
            $this.height($this.height() + heightDiff - 300);
        });
    },

    /**
     * Get the document object of an iframe.
     *
     * This function assumes the document might not be ready at the time the function is called, that's why the result
     * is returned to a callback function.
     * @param  {Window}   frame
     * @param  {Function} callback It will take a single argument (DOM Document).
     * @return {void}     The function itself doesn't return anything.
     */
    getFrameDocument: function(frame, callback){
        var doc;

        setTimeout(function(){
            if (!frame.location || !frame.location.href.match(/^http/)) { // If the iframe isn't loading the page yet.
                setTimeout(arguments.callee, 50);
            }
            else{
                doc = frame.document;
                callback(doc);
            }
        });
    },

    preInitSampleDataManager: function(){
    },

    initSampleDataManager: function(){
        var that = this;
        this.getSampleData();

        $('#parser-data-preview').on('change', '.selector select', function(){ 
            that.showSamplePageData(that.temporaryParserId, $('option:selected', this).val());
            that.setSamplePagePrevNextEnabled();
        });
        $('#parser-data-preview').on('click', '.selector a:not(.disabled)', that.setSamplePageSelectorValueFromPrevNext);

        $('#parser-data-preview .use-page').click(function(){
           alert('test'); 
        });
    },

    getSampleData: function(clearAll){
        that = PARSER_CREATOR;
        if (that.currentSamplePageId && that.temporaryParserId) {
            $.ajax({
                url: '/parser/parse-next-page',
                type: 'post',
                dataType: 'json',
                data: {
                    clear_all: clearAll,
                    parser_id: that.temporaryParserId,
                    current_sample_page_id: that.currentSamplePageId
                },
                success: function(res){
                    $('#parser-data-preview .count').text(res.stats.parsed + ' of ' + res.stats.total + ' pages parsed. See details here.'); 
                    // We can add the availabe results while some other pages are still parsed.
                    if (!that.setSamplePageSelectorTimeout) { 
                        that.setSamplePageSelectorTimeout = setTimeout(function(){ that.setSamplePageSelector(res.parsedPageIds) }, 1000);
                    }
                    // And if the last page has been reached, we need to make sure we set the selector regardless of the timeout indicator.
                    else if (res.stats.parsed === res.stats.total) {
                        setTimeout(function(){ that.setSamplePageSelector(res.parsedPageIds) }, 1000);
                    }
                    if (res.stats.parsed < res.stats.total) {
                        setTimeout(that.getSampleData);
                    }
                },
                error: function(e) {
                    genericError(e);
                }
            });
        }
    },

    setSamplePageSelector: function(parsedPageIds){
        var that = PARSER_CREATOR;
        clearTimeout(that.setSamplePageSelectorTimeout);
        var $select = $('#parser-data-preview .selector select');
        var selectVal = $('option:selected', $select).val();

        $('option[value!=""]', $select).remove();
        for (var i in parsedPageIds) {
            $('<option>').val(parsedPageIds[i]).text(parseInt(i, 10) + 1).appendTo($select);
        }

        var numOptions = $('option', $select).length;

        if (selectVal) {
            $select.val(selectVal);
        }
        else if (numOptions > 1) {
            $select.val($('option:eq(1)', $select).val()).trigger('change');
        }

        that.setSamplePagePrevNextEnabled();

        $('#parser-data-preview .selector select').trigger('change');
    },

    setSamplePagePrevNextEnabled: function(){
        var $select = $('#parser-data-preview .selector select');
        var curOptionIdx = $('option', $select).index($('option:selected', $select));
        var numOptions = $('option', $select).length;

        if (curOptionIdx < 2) {
            $('#parser-data-preview .prev').addClass('disabled');
        }
        else {
            $('#parser-data-preview .prev').removeClass('disabled');
        }

        if (curOptionIdx === numOptions - 1) {
            $('#parser-data-preview .next').addClass('disabled');
        }
        else {
            $('#parser-data-preview .next').removeClass('disabled');
        }
    },

    setSamplePageSelectorValueFromPrevNext: function(){
        var that = PARSER_CREATOR;

        var $select = $('#parser-data-preview .selector select');
        var curOptionIdx = $('option', $select).index($('option:selected', $select));
        var numOptions = $('option', $select).length;

        if ($(this).hasClass('prev')) {
            $select.val($('option:eq(' + (curOptionIdx - 1) + ')', $select).val()).trigger('change');
        }
        else if ($(this).hasClass('next')) {
            $select.val($('option:eq(' + (curOptionIdx + 1) + ')', $select).val()).trigger('change');
        }

        that.setSamplePagePrevNextEnabled();
    },

    showSamplePageData: function(parserId, pageId){
        that.dataGetAjax && that.dataGetAjax.abort();
        var $dataDiv = $('#parser-data-preview .data');
        $('dl *', $dataDiv).remove();
        $('.loading', $dataDiv).show();

        var ajaxStartTime = new Date().getTime();
        that.dataGetAjax = $.ajax({
            type: 'get',
            url: '/data/get',
            data: {
                parser_id: parserId,
                page_id: pageId
            },
            success: function(res){
                setTimeout(function(){
                    if (new Date().getTime() - ajaxStartTime > 300) { // Artificially force at least 0.3 second of waiting so that the interface doesn't blink.
                        $('.loading', $dataDiv).hide();
                        var $dl = $('dl', $dataDiv);
                        for (var i in res) {
                            $('<dt>').text(FIELDSET.fields[res[i].fieldset_field_id].name).appendTo($dl);
                            $('<dd>').text(res[i].content).appendTo($dl);
                        }
                    }
                    else {
                        setTimeout(arguments.callee, 100);
                    }
                });
            },
            error: function(res){ genericError(res) }
        })
    },

    // Things that can be initialised before the website view iframe is fully loaded.
    preInit: function(){
        this.preInitVariables();
        this.preInitWindowResize();
        this.preInitFieldsManager();
        this.preInitSampleDataManager();
    },

    // Things that can be initialised once the website view iframe is fully loaded and we can let the user control the interface.
    init: function(){
        // Some iframe data will be retrieved asynchronously. We'll initialise the iframe here (last line) and the rest
        // on a special event.
        var that = this;
        $(window).on('webview-init', function(){
            that.initFieldsManager();
            that.initSampleDataManager();
        });

        this.initFrame();
    }
}

$(function(){
    PARSER_CREATOR.preInit();
});